package jastgen.generator;

import java.io.*;
import java.util.*;

import jastgen.CompileException;
import jastgen.nodes.ClassDef;
import jastgen.nodes.Field;
import jastgen.nodes.Import;
import jastgen.nodes.InterfaceDef;
import jastgen.nodes.Module;
import jastgen.nodes.Type;

import static java.util.Arrays.*;
import static java.util.Collections.*;

/* TODO:
 * ・「import java.util.*;」他の追加は、ユーザーが書いていない場合に限定すべき。
 * ・「NodeReplacer.java」の型引数の型引数 (2回なのは誤植ではない) が無いという警告に対応する。
 * ・「VisitorImpl.java」にEのinLitが追加されないバグを修正する。
 * 　(バグでは無く、使用するものだけ実装するためか?)
 * 　(全パターンを実装させるか?)
 */
public class Generator {

	private final String dirPath;

	public Generator(
			final String dirPath)
	{
		this.dirPath = dirPath;
	}

	// Root of the generation.
	public void generate(Module node) throws IOException, CompileException {

		/* dump to debug
		for(final ClassDef c : node.getClassDefs()) {
			System.out.println(c.getClassName() + "{");
			for(final Field f : c.getMembers())
				System.out.println(f.getName());
			System.out.println("}");
		}
		*/

		final String packageNode    = node.getNamespace() + ".node";
		final String packageVisitor = node.getNamespace() + ".visitor";
		final String packageUtil    = node.getNamespace() + ".util";

		// Make output directory for node classes.
		if(new File(namespaceToFilePath(node.getNamespace())).mkdirs())
			;

		// These method calls must be 1st.
		// (Because these methods validate Module node)
		final Set<String> interfaceNames = interfaceNames(node);
		final Set<String> nodeTypes = classNames(interfaceNames, node);

		final List<String> visitParamTypes =
			getVisitParamTypes(node);

		// TODO: 改名する!!
		final List<String> visitParamTypes2 =
			getVisitParamTypes2(node);

		final List<String> visitReturnTypes =
			getVisitReturnTypes(visitParamTypes);

		final Map<String, List<String>> interfaceNameToMethodNames =
			interfaceNameToMethodNames(node);

		final List<Import> emptyList = emptyList();

		final Import impUtil = new Import(
				true,
				asList((node.getNamespace() + ".util.Util.*").split("\\.")));

		final List<Import> imports = imports(
				node.getNamespace() + ".visitor.Visitor",
				node.getNamespace() + ".visitor.VoidVisitor",
				node.getNamespace() + ".util.ComparableImpl");

		final List<Import> importsX = imports(
				node.getNamespace() + ".Context",
				node.getNamespace() + ".visitor.TreePrinter",
				node.getNamespace() + ".visitor.Visitor",
				node.getNamespace() + ".visitor.VoidVisitor");






		final List<Import> importsA = imports(classPathsInterface(    packageNode, node.getInterfaces()));  // node interfaces
		final List<Import> importsB = imports(classPathsInterfaceless(packageNode, node.getClassDefs()));   // interfaceless node classes
		final List<Import> importsC = imports(classPaths(             packageNode, node.getClassDefs()));   // all node classes
		final List<Import> importsD = imports(classPathsHasFields(    packageNode, node.getClassDefs()));   // node classes which has fields

		final List<Import> importsQ = append(importsA, importsB);  // node interface or interfaceless node classes
		final List<Import> imports6 = append(importsA, importsC);  // node interface or node classes






		final List<Import> imports7 = imports(packageNode + ".Node");

		final List<Import> importsInterface = append(node.getImports(), imports7, imports);


		final List<Import> imports8 = append(importsC, imports7);

		final List<Import> importsClassDef = append(
				node.getImports(),
				imports7,
				importsX,
				asList(impUtil),
				importsQ);


		final List<Import> importsDefaultContext = append(
				node.getImports(),
				importsQ);


		final List<Import> importsVisitorImpl = append(
				imports("java.util.*"),
				importsQ);

		final List<Import> imports2 = append(node.getImports(), imports6);

		final List<Import> importsNodeReplacer = append(
				imports("java.util.*", (node.getNamespace() + ".Context")),
				imports2);

		final List<Import> importsContext = append(
				imports2,
				imports(
						"java.util.Collection",
						"java.util.List",
						"java.util.Set",
						"java.util.SortedSet",
						"java.util.Map",
						"java.util.HashMap"));

		final List<Import> importsNodesCollector = append(imports(packageVisitor + ".VoidVisitorAdapter"), importsD);
		final List<Import> importsUtil = imports("java.util.*");
		final List<Import> importsComparableImpl = asList(impUtil);

		final List<Import> importsVoidVisitorAdapter =
			VoidVisitorAdapterBodyGenerator.importCollection(nodeTypes, node)
					? append(imports("java.util.Collection"), imports8)
					: imports8;

		final List<Import> importsTreePrinter = append(
				imports("java.io.BufferedWriter", "java.io.PrintWriter", "java.io.StringWriter"),
				importsVoidVisitorAdapter);

		final List<Import> importsNode = imports(
				packageVisitor + ".VoidVisitor");

		final List<Import> importsVoidVisitorImpl =
			append(imports("java.util.Collection"), imports7);














		// TODO: lintメソッドとしてチェック機能をまとめては?

		// インターフェースで定義されたフィールドは全て実装しなければならない
		for(final ClassDef c : node.getClassDefs()) {
			if(!c.hasInterface())
				continue;
			final List<String> interfaceMethods =
				interfaceNameToMethodNames.get(c.getInterfaceBaseName());
			for(final String im : interfaceMethods) {
				if(!contains(c, im))
					throw new CompileException(
							"Interface method 'get" + im + "();'"
							+ " isn't defined in class '"
							+ c.getBaseName() + "'!");
			}
		}

		// インターフェースメソッドはメソッド本体を持っていてはならない
		for(final InterfaceDef i : node.getInterfaces()) {
			for(final Field f : i.getFields())
				if(f.hasBody()) {
					final String im = f.getNameInMethod();
					throw new CompileException(
							"Interface method 'get" + im + "();'"
							+ " in '" + i.getBaseName() + "' has body!");
				}
		}















		for(final InterfaceDef i : node.getInterfaces()) {
			final String n = i.getBaseName();
			writeFile(
					packageNode + "." + n.toLowerCase(),
					importsInterface,
					n,
					new InterfaceBodyGenerator(visitReturnTypes, i));
		}

		// FIXME: Visitor, VoidVisitor
		// TODO: これを使って、インターフェースを持つノード型をコンテナの型引数にしていないかチェックしては?
		final ClassToInterface m = new ClassToInterface(node);
		for(final ClassDef c : node.getClassDefs()) {
			writeFile(
					packageNode + classPathMiddle(c),
					importsClassDef,
					c.getBaseName(),
					new ClassBodyGenerator(visitReturnTypes, m, interfaceNameToMethodNames, c));
		}








		writeFile(
				packageVisitor,
				importsC,
				"VoidVisitor",
				new VoidVisitorBodyGenerator(node));

		writeFile(
				packageVisitor,
				importsVoidVisitorImpl,
				"VoidVisitorImpl",
				new VoidVisitorImplBodyGenerator(nodeTypes, node));

		writeFile(
				packageVisitor,
				importsC,
				"VoidVisitorStub",
				new VoidVisitorStubBodyGenerator(node));

		writeFile(
				packageVisitor,
				importsC,
				"VoidVisitorAdapter",
				new VoidVisitorAdapterBodyGenerator(nodeTypes, node));





		writeFile(
				packageVisitor,
				importsC,
				"Visitor",
				new VisitorBodyGenerator(
						visitReturnTypes,
						node));

		writeFile(
				packageVisitor,
				importsVisitorImpl,
				"VisitorImpl",
				new VisitorImplBodyGenerator(
						visitReturnTypes,
						nodeTypes,
						interfaceNames,
						node));

		writeFile(
				packageVisitor,
				importsC,
				"VisitorStub",
				new VisitorStubBodyGenerator(
						visitReturnTypes,
						node));

		writeFile(
				packageVisitor,
				importsNodeReplacer,
				"NodeReplacer",
				new NodeReplacerBodyGenerator(
						visitParamTypes,
						nodeTypes,
						node));









		writeFile(
				packageVisitor,
				emptyList,
				"ValueVisitor",
				new ValueVisitorBodyGenerator(
					visitReturnTypes.size()));

		writeFile(
				packageVisitor,
				imports6,
				"ValueVisitorStub",
				new ValueVisitorStubBodyGenerator(
					node.getInterfaces(), node));










		writeFile(
				packageVisitor,
				importsTreePrinter,
				"TreePrinter",
				new TreePrinterBodyGenerator(nodeTypes, node));

		writeFile(
				node.getNamespace(),
				importsNodesCollector,
				"NodesCollector",
				new NodesCollectorBodyGenerator(node));







		writeFile(
				packageNode,
				importsNode,
				"Node",
				new NodeBodyGenerator());

		writeFile(
				node.getNamespace(),
				importsContext,
				"Context",
				new ContextBodyGenerator(node.getClassDefs()));

		writeFile(
				node.getNamespace(),
				importsDefaultContext,
				"DefaultContext",
				new DefaultContextBodyGenerator(node.getClassDefs()));

		writeFile(
				packageUtil,
				importsUtil,
				"Util",
				new UtilBodyGenerator());

		writeFile(
				packageUtil,
				importsComparableImpl,
				"ComparableImpl",
				new ComparableImplBodyGenerator());

		writeFile(
				node.getNamespace(),
				importsNodeReplacer,
				"Is",
				new IsBodyGenerator(node));
	}

	private boolean contains(
			final ClassDef c,
			final String im)
	{
		for(final Field f : c.getFields()) {
			if(f.getNameInMethod().equals(im))
				return true;
		}
		return false;
	}



	// root nodeがinterfaceの場合はContextクラスでインポートする
	private List<Import> importRootNodeIfNeeded(
			final List<Import> interfaces,
			final List<ClassDef> classDefs)
	{
		final String rootName = classDefs.get(0).getInterfaceOrClassBaseName();
		for(final Import i : interfaces) {
			final List<String> b = i.getBody();
			if(b.get(b.size()-1).equals(rootName))
				return asList(i);
		}
		return emptyList();
	}










	private Map<String, List<String>> interfaceNameToMethodNames(
			final Module node)
	{
		final Map<String, List<String>> xs =
			new HashMap<String, List<String>>();
		for(final InterfaceDef i : node.getInterfaces()) {
			xs.put(
					i.getBaseName(),
					getNameInMethod(i.getFields()));
		}
		return xs;
	}

	private List<String> getNameInMethod(
			final List<Field> fs)
	{
		final List<String> xs = new ArrayList<String>(fs.size());
		for(final Field f : fs)
			xs.add(f.getNameInMethod());
		return xs;
	}











	// ノードインターフェース群のクラスパス
	private List<String> classPathsInterface(
			final String packageNode,
			final List<InterfaceDef> is)
	{
		final List<String> xs = new ArrayList<String>(is.size());
		for(final InterfaceDef i : is) {
			final String n = i.getBaseName();
			xs.add(packageNode + "." + n.toLowerCase() + "." + n);
		}
		return xs;
	}

	// ノードクラス群のクラスパス
	private List<String> classPaths(
			final String packageNode,
			final List<ClassDef> cs)
	{
		final List<String> xs = new ArrayList<String>(cs.size());
		for(final ClassDef c : cs)
			xs.add(classPath(packageNode, c));
		return xs;
	}

	// インターフェースを持たないノードクラス群のクラスパス
	private List<String> classPathsInterfaceless(
			final String packageNode,
			final List<ClassDef> cs)
	{
		final List<String> xs = new ArrayList<String>(cs.size());
		for(final ClassDef c : cs) {
			if(!c.hasInterface())
				xs.add(classPath(packageNode, c));
		}
		return xs;
	}

	// フィールドを持つノードクラス群のクラスパス
	private List<String> classPathsHasFields(
			final String packageNode,
			final List<ClassDef> cs)
	{
		final List<String> xs = new ArrayList<String>(cs.size());
		for(final ClassDef c : cs) {
			if(c.hasNormalFields())
				xs.add(classPath(packageNode, c));
		}
		return xs;
	}

	private String classPath(
			final String packageNode,
			final ClassDef c)
	{
		return packageNode + classPathMiddle(c) + "." + c.getBaseName();
	}

	private String classPathMiddle(
			final ClassDef c)
	{
		if(!c.hasInterface())
			return "";
		return "." + c.getInterfaceBaseName().toLowerCase();
	}

	private List<Import> imports(
			final String... is)
	{
		return imports(asList(is));
	}

	private List<Import> imports(
			final List<String> is)
	{
		final List<Import> xs = new ArrayList<Import>(is.size());
		for(final String i : is)
			xs.add(new Import(false, asList(i.split("\\."))));
		return xs;
	}

	/**
	 * Collect all names of ast node classes.
	 * チェック機能も兼ねているので、generate処理の最初で使用する。
	 * 
	 * @param node
	 * @return
	 * @throws CompileException
	 */
	private static Set<String> classNames(
			final Set<String> names_,
			final Module node
		) throws CompileException {

		final Set<String> names = new HashSet<String>(names_);

		for(final ClassDef c : node.getClassDefs()) {
			if(c.hasInterface()) {
				final String n = c.getInterfaceBaseName();
				if(!names.contains(n))
					throw new CompileException(
						"Interface '" + n + "' wasn't defined!");
			}
			final String n = c.getBaseName();
			if(names.contains(n))
				throw new CompileException(
					"Class '" + n + "' defined twice!");
			names.add(n);
		}

		return names;
	}

	/**
	 * Collect all names of ast node interfaces.
	 * チェック機能も兼ねているので、generate処理の最初で使用する。
	 * 
	 * @param node
	 * @return
	 * @throws CompileException
	 */
	private static Set<String> interfaceNames(
			final Module node
		) throws CompileException {

		final Set<String> names = new HashSet<String>();

		for(final InterfaceDef i : node.getInterfaces()) {
			final String n = i.getBaseName();
			if(names.contains(n))
				throw new CompileException(
					"Interface '" + n + "' defined twice!");
			names.add(n);
		}

		return names;
	}

	// インターフェースを持たないノードクラスと
	// 全てのインターフェースの名前を集める。
	// チェックは行わない。
	//
	// visitorReturnTypes
	private List<String> getVisitParamTypes(
			final Module node)
	{
		final List<String> names = new LinkedList<String>();

		for(final InterfaceDef i : node.getInterfaces()) {
			names.add(i.getBaseName());
		}

		for(final ClassDef c : node.getClassDefs()) {
			if(!c.hasInterface())
				names.add(c.getBaseName());
		}

		return names;
	}

	// インターフェースを持たないノードクラスと
	// 全てのインターフェースの名前 (型引数付き) を集める。
	// チェックは行わない。
	//
	// visitorReturnTypes
	// TODO: 改名する!!
	private List<String> getVisitParamTypes2(
			final Module node)
	{
		final List<String> names = new LinkedList<String>();

		for(final InterfaceDef i : node.getInterfaces()) {
			names.add(i.getName());
		}

		for(final ClassDef c : node.getClassDefs()) {
			if(!c.hasInterface())
				names.add(c.getName());
		}

		return names;
	}

	private List<String> getVisitReturnTypes(
			final List<String> ts)
	{
		final List<String> xs = new ArrayList<String>(ts.size());
		for(final String t : ts)
			xs.add(toVisitReturnType(t));
		return xs;
	}

	static String toVisitReturnType(final String s) {
		return "T" + s;
	}

	private void writeFile(
			final String        namespace,
			final List<Import>  imports,
			final String        className,
			final BodyGenerator body
		) throws IOException {

		PrintWriter out = null;
		try {
			final File f = new File(classInfoToFilePath(namespace, className));
			f.getParentFile().mkdirs();
			out = new PrintWriter(new FileWriter(f));

			out.println("package " + namespace + ";");
			out.println();

			if(!imports.isEmpty()) {
				for(final Import i : imports)
					printImport(out, i);
				out.println();
			}

			body.generate(out);

		} finally {
			if(out != null) {
				out.flush();
				out.close();
			}
		}
	}

	private void printImport(
			final PrintWriter out,
			final Import i)
	{
		out.print("import ");
		if(i.isStatic())
			out.print("static ");
		boolean is1st = true;
		for(final String s : i.getBody()) {
			if(!is1st) out.print(".");
			is1st = false;
			out.print(s);
		}
		out.println(";");
	}

	private String namespaceToFilePath(
			final String namespace)
	{
		return dirPath + "/" + namespace.replaceAll("\\.", "/");
	}

	private String classInfoToFilePath(
			String namespace,
			String className)
	{
		return namespaceToFilePath(namespace) + "/" + className + ".java";
	}



	// get getter's prefix by type
	static String getOrIs(final Field f) {
		final Type t = f.getType();
		if(t.getName().equals("boolean"))
			return "is";
		return "get";
	}





/*
	private static <T> List<T> append(
			final T lhs,
			final List<T> rhs)
	{
		final List<T> l1 = new ArrayList<T>(1 + rhs.size());
		l1.add(lhs);
		l1.addAll(rhs);
		return l1;
	}
*/
	private static <T> List<T> append(
			final List<T> lhs,
			final List<T> rhs)
	{
		final List<T> l1 = new ArrayList<T>(lhs.size() + rhs.size());
		l1.addAll(lhs);
		l1.addAll(rhs);
		return l1;
	}

	private static <T> List<T> append(
			final List<T> lst0,
			final List<T> lst1,
			final List<T> lst2)
	{
		final List<T> l1 = new ArrayList<T>(lst0.size() + lst1.size() + lst2.size());
		l1.addAll(lst0);
		l1.addAll(lst1);
		l1.addAll(lst2);
		return l1;
	}

	private static <T> List<T> append(
			final List<T> lst0,
			final List<T> lst1,
			final List<T> lst2,
			final List<T> lst3)
	{
		final List<T> l1 = new ArrayList<T>(lst0.size() + lst1.size() + lst2.size() + lst3.size());
		l1.addAll(lst0);
		l1.addAll(lst1);
		l1.addAll(lst2);
		l1.addAll(lst3);
		return l1;
	}

	private static <T> List<T> append(
			final List<T> lst0,
			final List<T> lst1,
			final List<T> lst2,
			final List<T> lst3,
			final List<T> lst4)
	{
		final List<T> l1 = new ArrayList<T>(
				lst0.size() +
				lst1.size() +
				lst2.size() +
				lst3.size() +
				lst4.size());
		l1.addAll(lst0);
		l1.addAll(lst1);
		l1.addAll(lst2);
		l1.addAll(lst3);
		l1.addAll(lst4);
		return l1;
	}

	private static <T> List<T> append(
			final List<T> lst0,
			final List<T> lst1,
			final List<T> lst2,
			final List<T> lst3,
			final List<T> lst4,
			final List<T> lst5)
	{
		final List<T> l1 = new ArrayList<T>(
				lst0.size() +
				lst1.size() +
				lst2.size() +
				lst3.size() +
				lst4.size() +
				lst5.size());
		l1.addAll(lst0);
		l1.addAll(lst1);
		l1.addAll(lst2);
		l1.addAll(lst3);
		l1.addAll(lst4);
		l1.addAll(lst5);
		return l1;
	}

	// TODO: これは直後の改行無しで、直下のは改行有り。統一しては?
	static void printTypeParams(
			final PrintWriter out,
			final List<String> ts,
			final String indent)
	{
		out.print("<");

		boolean is1st = true;
		for(final String t : ts) {
			if(!is1st) {
				out.println(",");
				out.print(indent);
			}
			is1st = false;
			out.print(t);
		}

		out.print(">");
	}

	static void printTypeParams(
			final PrintWriter out,
			final List<String> ts)
	{
		out.println("<");

		boolean is1st = true;
		for(final String t : ts) {
			if(!is1st)
				out.println(",");
			is1st = false;
			out.print("		" + t);
		}

		out.println(">");
	}

	// インスタンス変数名と重複しない名前を返す
	static String unique(
			final ClassDef node,
			final String id)
	{
		return unique(node.getFields(), id);
	}

	static String unique(
			final List<Field> fs,
			final String id)
	{
		String s = id;
		for(int i=0; true; ++i) {
			if(isUsed(fs, s)) {
				s = id + i;
				continue;
			}
			return s;
		}
	}

	private static boolean isUsed(
			final List<Field> fs,
			final String s)
	{
		for(final Field f : fs)
			if(s.equals(f.getName()))  // この行だけが下と違う
				return true;
		return false;
	}

	// メソッド名と重複しない名前を返す
	static String uniqueNameInMethod(
			final ClassDef node,
			final String id)
	{
		return uniqueNameInMethod(node.getFields(), id);
	}

	private static String uniqueNameInMethod(
			final List<Field> fs,
			final String id)
	{
		String s = id;
		for(int i=0; true; ++i) {
			if(isUsedNameInMethod(fs, s)) {
				s = id + i;
				continue;
			}
			return s;
		}
	}

	private static boolean isUsedNameInMethod(
			final List<Field> fs,
			final String s)
	{
		for(final Field f : fs)
			if(s.equals(f.getNameInMethod()))  // この行だけが上と違う
				return true;
		return false;
	}
}
